package demo.ssh2;

import com.jcraft.jsch.Channel;
import com.jcraft.jsch.ChannelExec;
import com.jcraft.jsch.ChannelSftp;
import com.jcraft.jsch.JSch;
import com.jcraft.jsch.JSchException;
import com.jcraft.jsch.Session;
import com.jcraft.jsch.SftpException;
import com.jcraft.jsch.UserInfo;
import org.junit.Test;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.Properties;
import java.util.Vector;

/**
 * JSch 是Java Secure Channel的缩写。JSch是一个SSH2的纯Java实现。它允许你连接到一个SSH服务器，并且可以使用端口转发，X11转发，文件传输等，
 * 当然你也可以集成它的功能到你自己的应用程序。常用的有三种通道，即ChannelShell、ChannelExec、ChannelSftp，
 * 前两类用于执行命令(命令可以是shell语句，也可以是python xxx.py)，后一种是用于上传下载文件。
 * <p>
 * ChannelShell和ChannelExec的区别:
 * 前者是交互式的，在channel.connect()之前，需要获取outputStream和inputStream，然后outputstream发送命令，
 * 从instream中读取命令的结果(注意，发送命令之后，读取命令之前要等待一会儿，一般需要写个循环判断，每秒读一次，
 * 根据实际情况设置xx秒超时退出)，但是有个坏处是，它执行就像登陆到vm上打印的信息一样，无论执行命令后的任何信息，
 * 它都会通过instream返回到客户端来，而你可能仅仅需要命令执行后的结果；于是就有了后者，非交互的，
 * 一次通道执行一条命令(当然如果你组合的好，也可以多条，反正就是一个字符串，一次交互，偷偷的告诉你，这一个python脚本，
 * 下发的命令去执行这个脚本，可以做好多好多事情哦)，好处是，它返回的信息非常干净，只返回标准输出，标准错误是不返回的，
 * 这时你可以利用python的print，正确时你print正确信息，错误时print错误信息，客户端都能获取到结果啦(因为print是标准输出)。
 * <p>
 * 使用步骤:
 * <li>1、new一个JSch对象；
 * <li>2、从JSch对象中获取Session，用于连接，并设置连接信息
 * (根据SSH的连接原理，有两种方式，一是用户名+密码，二是用户名+privateKey+passphrase，
 * 第二种方式要在第1步设置，即jsch.addIdentity(xxx))；
 * <li>3、使用session对象调用opnChannel("xxx")打开通信信道，并连接；
 * <li>4、后面就是不同的channel，不同的操作啦。
 */
public class JSchDemo {
    private static String username = "Stan";
    private static String host = "192.168.60.242";
    private static String password = "123456";
    private static int port = 22;
    // 私钥
    private static String prvFile = "/Users/xmly/.ssh/id_rsa";

    static {
        JSch.setConfig("server_host_key", JSch.getConfig("server_host_key") + ",ssh-rsa");
        JSch.setConfig("PubkeyAcceptedAlgorithms", JSch.getConfig("PubkeyAcceptedAlgorithms") + ",ssh-rsa");
    }

    static UserInfo userInfo = new UserInfo() {
        @Override
        public String getPassphrase() {
            System.out.println("getPassphrase");
            return null;
        }

        @Override
        public String getPassword() {
            System.out.println("getPassword");
            return null;
        }

        @Override
        public boolean promptPassword(String s) {
            System.out.println("promptPassword:" + s);
            return false;
        }

        @Override
        public boolean promptPassphrase(String s) {
            System.out.println("promptPassphrase:" + s);
            return false;
        }

        @Override
        public boolean promptYesNo(String s) {
            System.out.println("promptYesNo:" + s);
            return true;// notice here!
        }

        @Override
        public void showMessage(String s) {
            System.out.println("showMessage:" + s);
        }
    };

    static void closeChannel(Channel channel) {
        if (channel != null) {
            if (channel.isConnected()) {
                channel.disconnect();
            }
        }
    }

    static void closeSession(Session session) {
        if (session != null) {
            if (session.isConnected()) {
                session.disconnect();
            }
        }
    }


    /**
     * JSch 使用 shell 执行命令，有两种方法:
     * <li>ChannelExec: 一次执行一条命令
     * <li>ChannelShell: 可执行多条命令
     * <p>
     * <li>每个命令之间用 ; 隔开。说明：各命令的执行给果，不会影响其它命令的执行。换句话说，各个命令都会执行，但不保证每个命令都执行成功。
     * <li>每个命令之间用 && 隔开。说明：若前面的命令执行成功，才会去执行后面的命令。这样可以保证所有的命令执行完毕后，执行过程都是成功的。
     * <li>每个命令之间用 || 隔开。说明：|| 是或的意思，只有前面的命令执行失败后才去执行下一条命令，直到执行成功一条命令为止。
     * </p>
     * <p>
     * 需要注意的点
     * 如果需要后台执行某个命令，不能直接 <命令> + & 的方式执行，这样在 JSch 中不生效，
     * 需要写成这样的格式：<命令> > /dev/null 2>&1 &。
     * 比如要后台执行 sleep 60，需要写成 sleep 60 > /dev/null 2>&1
     */
    @Test
    public void testChannelExec() throws JSchException, IOException {
        String cmd = "free -mh";
        JSch jSch = new JSch();
        Session session = null;
        ChannelExec channelExec = null;
        BufferedReader inputStreamReader = null;
        BufferedReader errInputStreamReader = null;
        StringBuilder runLog = new StringBuilder("");
        StringBuilder errLog = new StringBuilder("");
        try {
            // 1. 获取 ssh session
            session = jSch.getSession(username, host, port);
            session.setPassword(password);
            session.setTimeout(3000);
            session.setConfig("StrictHostKeyChecking", "no");
            session.connect();  // 获取到 ssh session

            // 2. 通过 exec 方式执行 shell 命令
            channelExec = (ChannelExec) session.openChannel("exec");
            channelExec.setCommand(cmd);
            channelExec.connect();  // 执行命令

            // 3. 获取标准输入流
            inputStreamReader = new BufferedReader(new InputStreamReader(channelExec.getInputStream()));
            // 4. 获取标准错误输入流
            errInputStreamReader = new BufferedReader(new InputStreamReader(channelExec.getErrStream()));

            // 5. 记录命令执行 log
            String line = null;
            while ((line = inputStreamReader.readLine()) != null) {
                runLog.append(line).append("\n");
            }

            // 6. 记录命令执行错误 log
            String errLine = null;
            while ((errLine = errInputStreamReader.readLine()) != null) {
                errLog.append(errLine).append("\n");
            }

            // 7. 输出 shell 命令执行日志
            System.out.println("exitStatus=" + channelExec.getExitStatus() + ", openChannel.isClosed="
                    + channelExec.isClosed());
            System.out.println("命令执行完成，执行日志如下:");
            System.out.println(runLog);
            System.out.println("命令执行完成，执行错误日志如下:");
            System.out.println(errLog);
        } finally {
            try {
                if (inputStreamReader != null) {
                    inputStreamReader.close();
                }
                if (errInputStreamReader != null) {
                    errInputStreamReader.close();
                }

                if (channelExec != null) {
                    channelExec.disconnect();
                }
                if (session != null) {
                    session.disconnect();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * 通过私钥
     */
    @Test
    public void testChannelExecWithPrivateKey() throws JSchException, IOException {
        String cmd = "free -mh";

        Session session = null;
        ChannelExec channelExec = null;
        BufferedReader inputStreamReader = null;
        BufferedReader errInputStreamReader = null;
        StringBuilder runLog = new StringBuilder("");
        StringBuilder errLog = new StringBuilder("");
        try {
            JSch jSch = new JSch();
            jSch.addIdentity(prvFile);
            // 1. 获取 ssh session
            session = jSch.getSession("root", "192.168.3.52", port);

            session.setTimeout(3000);
            session.setConfig("StrictHostKeyChecking", "no");
            session.connect();  // 获取到 ssh session

            // 2. 通过 exec 方式执行 shell 命令
            channelExec = (ChannelExec) session.openChannel("exec");
            channelExec.setCommand(cmd);
            channelExec.connect();  // 执行命令

            // 3. 获取标准输入流
            inputStreamReader = new BufferedReader(new InputStreamReader(channelExec.getInputStream()));
            // 4. 获取标准错误输入流
            errInputStreamReader = new BufferedReader(new InputStreamReader(channelExec.getErrStream()));

            // 5. 记录命令执行 log
            String line = null;
            while ((line = inputStreamReader.readLine()) != null) {
                runLog.append(line).append("\n");
            }

            // 6. 记录命令执行错误 log
            String errLine = null;
            while ((errLine = errInputStreamReader.readLine()) != null) {
                errLog.append(errLine).append("\n");
            }

            // 7. 输出 shell 命令执行日志
            System.out.println("exitStatus=" + channelExec.getExitStatus() + ", openChannel.isClosed="
                    + channelExec.isClosed());
            System.out.println("命令执行完成，执行日志如下:");
            System.out.println(runLog.toString());
            System.out.println("命令执行完成，执行错误日志如下:");
            System.out.println(errLog.toString());
        } finally {
            try {
                if (inputStreamReader != null) {
                    inputStreamReader.close();
                }
                if (errInputStreamReader != null) {
                    errInputStreamReader.close();
                }

                if (channelExec != null) {
                    channelExec.disconnect();
                }
                if (session != null) {
                    session.disconnect();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * SFTP是Secure File Transfer Protocol的缩写，安全文件传送协议。可以为传输文件提供一种安全的加密方法。
     * SFTP 为 SSH的一部份，是一种传输文件到服务器的安全方式。SFTP是使用加密传输认证信息和传输的数据，所以，使用SFTP是非常安全的。
     * 但是，由于这种传输方式使用了加密/解密技术，所以传输效率比普通的FTP要低得多。
     * ChannelSftp类是JSch实现SFTP核心类，它包含了所有SFTP的方法，如：
     * <li> put()：      文件上传
     * <li> get()：      文件下载
     * <li> cd()：       进入指定目录
     * <li> ls()：       得到指定目录下的文件列表
     * <li> rename()：   重命名指定文件或目录
     * <li> rm()：       删除指定文件
     * <li> mkdir()：    创建目录
     * <li> rmdir()：    删除目录
     * <p>
     * Sch支持三种文件传输模式：
     *
     * <li> OVERWRITE:
     * 完全覆盖模式，这是JSch的默认文件传输模式，即如果目标文件已经存在，传输的文件将完全覆盖目标文件，产生新的文件。
     * <li> RESUME:
     * 恢复模式，如果文件已经传输一部分，这时由于网络或其他任何原因导致文件传输中断，如果下一次传输相同的文件，
     * 则会从上一次中断的地方续传。
     * <li> APPEND: 追加模式，如果目标文件已存在，传输的文件将在目标文件后追加。
     */
    public void testSFTP() {
        ChannelSftp channelSftp = null;
        Channel channel = null;
        Session session = null;
        try {
            // 创建JSch对象
            JSch jsch = new JSch();
            // 根据用户名，主机ip，端口获取一个Session对象
            session = jsch.getSession(username, host, port);
            // 设置密码
            session.setPassword(password);
            Properties properties = new Properties();
            properties.put("StrictHostKeyChecking", "no");
            // 为Session对象设置properties
            session.setConfig(properties);
            // 设置timeout时间
            session.setTimeout(9000);
            //            通过Session建立链接
            session.connect();
            System.out.println("连接成功");
            // 打开SFTP通道
            channel = session.openChannel("sftp");
            // 建立SFTP通道的连接
            channel.connect();

            channelSftp = (ChannelSftp) channel;
            Vector vector = channelSftp.ls("/home/docker/opt/pcre-8.40");
            for (Object item : vector) {
                ChannelSftp.LsEntry entry = (ChannelSftp.LsEntry) item;
                System.out.println(entry.getFilename());
            }
        } catch (JSchException e) {
            e.printStackTrace();
        } catch (SftpException e) {
            e.printStackTrace();
        } finally {
            closeChannel(channelSftp);
            closeChannel(channel);
            closeSession(session);
        }

    }

    /**
     * 对于ChannelShell，以输入流的形式，可执行多条指令，这就像在本地计算机上使用交互式shell（它通常用于：交互式使用）。
     * 如要要想停止，有两种方式：
     *
     * <li>1、发送一个exit命令，告诉程序本次交互结束；
     * <li>2、使用字节流中的available方法，来获取数据的总大小，然后循环去读。
     */
    public void testChannelShell() {
        try {
            JSch jsch = new JSch();
            Session session = jsch.getSession(username, host, port);
            session.setPassword(password);
            session.setUserInfo(userInfo);

            // It must not be recommended, but if you want to skip host-key check,
            // invoke following,
            // session.setConfig("StrictHostKeyChecking", "no");

            // session.connect();
            session.connect(30000); // making a connection with timeout.

            Channel channel = session.openChannel("shell");

            // Enable agent-forwarding.
            // ((ChannelShell)channel).setAgentForwarding(true);

            channel.setInputStream(System.in);
            /*
             * // a hack for MS-DOS prompt on Windows. channel.setInputStream(new
             * FilterInputStream(System.in){ public int read(byte[] b, int off, int
             * len)throws IOException{ return in.read(b, off, (len>1024?1024:len)); } });
             */

            channel.setOutputStream(System.out);

            /*
             * // Choose the pty-type "vt102". ((ChannelShell)channel).setPtyType("vt102");
             */

            /*
             * // Set environment variable "LANG" as "ja_JP.eucJP".
             * ((ChannelShell)channel).setEnv("LANG", "ja_JP.eucJP");
             */

            // channel.connect();
            channel.connect(3 * 1000);
        } catch (Exception e) {
            System.err.println(e);
        }
    }


}
